package com.maimao.auth.service;

import com.alibaba.fastjson.JSON;
import com.maimao.auth.model.AuthToken;
import com.maimao.core.constant.GrantTypeEnum;
import com.maimao.core.constant.RedisKeys;
import com.maimao.core.constant.ServiceContainer;
import com.maimao.core.enums.AuthEnum;
import com.maimao.core.exception.AuthException;
import com.maimao.core.model.auth.AuthUser;
import com.maimao.core.utils.Oauth2Util;
import com.maimao.core.utils.RegexUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Service;
import org.springframework.util.Base64Utils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestTemplate;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.net.URI;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * @author MaoLin Wang
 * @date 2020/11/30 2:34 下午
 */
@Slf4j
@Service
public class AuthService {
    public static final String APPLY_TOKEN_URI = "/auth/oauth/token?type=";
    public static final String ACCESS_TOKEN = "access_token";
    public static final String REFRESH_TOKEN = "refresh_token";
    public static final String JTI = "jti";

    @Value("${auth.expireRedis}")
    private Integer expireRedis;

    private final LoadBalancerClient loadBalancerClient;

    private final RestTemplate restTemplate;

    private final StringRedisTemplate stringRedisTemplate;

    public AuthService(LoadBalancerClient loadBalancerClient, RestTemplate restTemplate, StringRedisTemplate stringRedisTemplate) {
        this.loadBalancerClient = loadBalancerClient;
        this.restTemplate = restTemplate;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 用户名 + 密码 目标仅用于后台登录
     *
     * @param username     用户名
     * @param password     密码
     * @param clientId     客户端id
     * @param clientSecret 客户端密码
     * @param type         用户类型 sys：后台
     * @return AuthToken
     */
    public AuthToken sysLogin(String username, String password, String clientId, String clientSecret, String type) {
        AuthToken authToken = applyToken(username, password, null, null, clientId, clientSecret, type, GrantTypeEnum.PASSWORD.getType());
        return saveToken(authToken);

    }

    /**
     * 用户名/手机/邮箱 + 密码 + 图形验证码登录 会员三个都允许 企业只允许邮箱登录
     *
     * @param account 用户名/手机/邮箱
     * @param type    member:会员 company:企业
     */
    public AuthToken webLogin(String account, String password, String code, String ip, String clientId, String clientSecret, String type) {
        String grantType;
        if (RegexUtils.checkMobile(account)) {
            grantType = GrantTypeEnum.MOBILE_PWD_CODE.getType();
        } else if (RegexUtils.checkEmail(account)) {
            grantType = GrantTypeEnum.EMAIL_PWD_CODE.getType();
        } else {
            grantType = GrantTypeEnum.USERNAME_PWD_CODE.getType();
        }
        AuthToken authToken = applyToken(account, password, code, ip, clientId, clientSecret, type, grantType);
        return saveToken(authToken);
    }

    /**
     * 手机+验证码登录认证 目前只允许会员进行手机免密登录
     *
     * @param mobile 手机
     * @param code   验证码
     * @param type   member:普通用户 company：企业用户
     */
    public AuthToken mobileLogin(String mobile, String code, String clientId, String clientSecret, String type) {
        AuthToken authToken = applyToken(mobile, null, code, null, clientId, clientSecret, type, GrantTypeEnum.MOBILE_CODE.getType());
        return saveToken(authToken);

    }

    private AuthToken saveToken(AuthToken authToken) {
        if (authToken == null) {
            throw new AuthException(AuthEnum.APPLY_TOKEN_FAIL);
        }
        //jti短令牌
        String shortToken = authToken.getAccess_token();
        String tokenDetail = JSON.toJSONString(authToken);
        boolean res = save2Redis(shortToken, tokenDetail, expireRedis);
        if (res) {
            return authToken;
        }
        throw new AuthException(AuthEnum.SAVE_TOKEN_FAIL);
    }

    private boolean save2Redis(String shortToken, String tokenDetail, Integer expire) {
        String key = RedisKeys.USER_TOKEN + shortToken;
        stringRedisTemplate.boundValueOps(key).set(tokenDetail, expire, TimeUnit.SECONDS);
        Long last = stringRedisTemplate.getExpire(key, TimeUnit.SECONDS);
        return last != null && last > 0;

    }

    /**
     * 申请令牌
     *
     * @return 令牌信息
     */
    private AuthToken applyToken(String username, String password, String code, String ip, String clientId, String clientSecret, String type, String grantType) {
        // 从Nacos中获取认证服务的一个实例的地址
        ServiceInstance serviceInstance = loadBalancerClient.choose(ServiceContainer.AUTH_SERVICE);
        if (serviceInstance == null) {
            throw new AuthException(AuthEnum.NO_SERVICE_FOUND);
        }
        // uri格式: http://ip:port
        URI uri = serviceInstance.getUri();
        // 令牌申请的地址 http://ip:port/auth/oauth/token
        // 所在类：TokenEndpoint.postAccessToken
        String authUrl = uri + APPLY_TOKEN_URI + type;
        // 请求头
        LinkedMultiValueMap<String, String> header = new LinkedMultiValueMap<>();
        String httpBasic = getHttpBasic(clientId, clientSecret);
        header.add("Authorization", httpBasic);

        // 请求体
        LinkedMultiValueMap<String, String> body = new LinkedMultiValueMap<>();

        body.add("grant_type", grantType);
        body.add("username", username);
        if (password != null) {
            body.add("password", password);
        }
        if (code != null) {
            body.add("code", code);
        }
        if (ip != null) {
            body.add("ip", ip);
        }

        HttpEntity<MultiValueMap<String, String>> httpEntity = new HttpEntity<>(body, header);

        // 设置restTemplate远程调用时候，对400和401不让报错，正确返回数据
        restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {
            @Override
            public void handleError(ClientHttpResponse response) throws IOException {
                if (response.getRawStatusCode() != 400 && response.getRawStatusCode() != 401) {
                    super.handleError(response);
                }
            }
        });

        ResponseEntity<Map> exchange = restTemplate.exchange(authUrl, HttpMethod.POST, httpEntity, Map.class);

        //申请令牌信息
        Map tokenMap = exchange.getBody();
        if (tokenMap == null ||
                tokenMap.get(ACCESS_TOKEN) == null ||
                tokenMap.get(REFRESH_TOKEN) == null ||
                tokenMap.get(JTI) == null) {
            if (tokenMap != null) {
                log.info("tokenMap:{}", tokenMap);
            }

            //解析spring security返回的错误信息
            if (tokenMap != null && tokenMap.get("error_description") != null) {
                String error_description = (String) tokenMap.get("error_description");
                if (error_description.indexOf("UserDetailsService returned null") >= 0) {
                    throw new AuthException(AuthEnum.USER_NOT_FOUNT);
                } else if (error_description.contains("用户名或密码错误")) {
                    throw new AuthException(AuthEnum.LOGIN_ERROR);
                } else if (error_description.contains("Internal Server Error")) {
                    throw new AuthException("请稍后再试", 500);
                } else {
                    log.error("申请令牌失败：{}", error_description);

                    throw new AuthException(error_description, 403);
                }
            }
            return null;
        }

        AuthToken authToken = AuthToken.builder()
                .access_token((String) tokenMap.get(JTI))
                .jwt_token((String) tokenMap.get(ACCESS_TOKEN))
                .refresh_token((String) tokenMap.get(REFRESH_TOKEN))
                .build();

        checkRepeatLogin(type, authToken);
        return authToken;


    }

    /**
     * 判断重复登录
     *
     * @param type
     * @param authToken
     */
    private void checkRepeatLogin(String type, AuthToken authToken) {
        Map<String, Object> userInfos = Oauth2Util.getJwtClaimsFromJwt((String) authToken.getJwt_token());
        String id = (String) userInfos.get("id");
        String utype = (String) userInfos.get("type");
        if (id != null && !StringUtils.isEmpty(utype)) {
            String key = String.format(RedisKeys.REPEAT_LOGIN, id, utype);
//            String oldValue = stringRedisTemplate.opsForValue().get(key);
            //已经登录 就用新的token覆盖
              /*  if (utype.equals("sys")) {
                    //后台只允许登录一次
                    throw new AuthException(AuthEnum.LOGIN_REPEAT);
                }*/

            stringRedisTemplate.opsForValue().set(key, authToken.getAccess_token(), expireRedis, TimeUnit.SECONDS);


        }
    }

    /**
     * 获取http basic的编码 格式：Basic Base64.encode(clientId:clientSecret)
     *
     * @param clientId     客户端id
     * @param clientSecret 客户端密码
     */
    private String getHttpBasic(String clientId, String clientSecret) {
        String string = clientId + ":" + clientSecret;
        //将串进行base64编码
        byte[] encode = Base64Utils.encode(string.getBytes());
        return "Basic " + new String(encode);
    }

    public void logout(String token, HttpServletRequest request) {
        String key = RedisKeys.USER_TOKEN + token;
        stringRedisTemplate.delete(key);
        AuthUser authUser = Oauth2Util.getUserFromRequest(request);
        if (null == authUser) {
            log.warn("authUser is null ,log out warning,token:{}", token);
            return;
        }
        String repeatKey = String.format(RedisKeys.REPEAT_LOGIN, authUser.getId(), authUser.getType());
        stringRedisTemplate.delete(repeatKey);
    }


    public String getJwt(String cookie) {
        String key = RedisKeys.USER_TOKEN + cookie;
        String tokenStr = stringRedisTemplate.opsForValue().get(key);
        if (StringUtils.isEmpty(tokenStr)) {
            return null;
        }

        AuthToken authToken = JSON.parseObject(tokenStr, AuthToken.class);
        return authToken.getJwt_token();
    }
}
